#include "UdpConnMgr.hpp"
#include "Device.hpp"
#include "DeviceSpecInfo.hpp"
#include "Log.hpp"
#include "ErrnoException.hpp"
#include "ExceptionUtil.hpp"
#include "NetUtil.hpp"
#include "IncomingPacket.hpp"
#include "UdpDeviceInfo.hpp"
#include "messages/FindDevicesMsg.hpp"
#include "messages/FindDevicesReply.hpp"

#include <stdio.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <fcntl.h>
#include <errno.h>

#include <iostream>
#include <memory>
#include <ctime>

static const unsigned int BROADCAST_PORT = 22222;

/**
 * The time we wait for the devices to reply to the broadcast packet in seconds
 */
static const time_t DEVS_WAIT_TIME = 2;

/**
 * How long the connection manager waits for the devices to
 * reply to the broadcast packet
 */
static const unsigned int FIND_DEV_TIMEOUT_MS = 3000;

/*
int sock;
struct sockaddr_in echoclient;
char buffer[BUFFSIZE];
unsigned int echolen, clientlen, serverlen;
int received = 0;



serverlen = sizeof(echoserver);

   while (1) {
   clientlen = sizeof(echoclient);
   if ((received = recvfrom(sock, buffer, BUFFSIZE, 0,
   (struct sockaddr *) &echoclient,
   &clientlen)) < 0) {
   Die("Failed to receive message");
   }
   fprintf(stderr,
   "Client connected: %s\n", inet_ntoa(echoclient.sin_addr));
   if (sendto(sock, buffer, received, 0,
   (struct sockaddr *) &echoclient,
   sizeof(echoclient)) != received) {
   Die("Mismatch in number of echo'd bytes");
   }
   }

 */


namespace kieper {

    using std::time;

    UdpConnMgr::UdpConnMgr(const char* host, unsigned int port) : 
        host_(host), port_(port) {}

    void UdpConnMgr::doInitialize() {
        createServerSock();
        deviceSock_ = netutil::createUdpSock();
    }

    DeviceMapPtr UdpConnMgr::doFindDevices() {
        netutil::setNonBlocking(serverSock_);
        // The socket which will be used for sending the broadcast message
        
        int clientSock = netutil::createUdpSock();
        netutil::setBroadcast(clientSock);

        sockaddr_in clientAddr;
        memset(&clientAddr, 0, sizeof(clientAddr));
        clientAddr.sin_family = AF_INET;
        clientAddr.sin_addr.s_addr = htonl(INADDR_BROADCAST);
        clientAddr.sin_port = htons(BROADCAST_PORT);

        FindDevicesMsg msg(host_, port_);
        Packet& packet = msg.getPacket();

        int sendSize = packet.getTotalSize();
        LOG_TRACE("sending packet of size %d", sendSize);

        if (sendto(clientSock, packet.getBuffer(), sendSize, 0, 
            reinterpret_cast<sockaddr*> (&clientAddr), sizeof(clientAddr)) != sendSize) {
            THROW(ErrnoException, "Failed to send FindDeviceMsg packet");
        }
        
        sockaddr_in deviceAddr;
        int received;
        
        socklen_t addrLen = sizeof(deviceAddr);

        DeviceMap* deviceMap = new DeviceMap;
        
        // Create and start timer
        time_t startTime = time(NULL);
        while (time(NULL) != startTime + DEVS_WAIT_TIME) {
            if ((received = recvfrom(serverSock_, buffer_, BUFFER_SIZE, 0, 
                reinterpret_cast<sockaddr*> (&deviceAddr), &addrLen)) < 0) {
                if (errno == EAGAIN) {
                    continue;
                } else {
                    THROW(ErrnoException, "Failed to receive packet");
                }
            } else {
                IncomingPacket packet(buffer_, received - Packet::HEADER_SIZE);
                LOG_TRACE("Received packet type = %d, del = %d, scope = %d, dir = %d, \
                        msgnum = %u", packet.getType(), packet.getDelivery(), packet.getScope(),
                        packet.getDirection(), packet.getMessageNumber());

                if  (packet.getType() == Packet::TYPE_CONTROL &&
                    packet.getDelivery() == Packet::DELIVERY_ASYNC &&
                    packet.getScope() == Packet::SCOPE_GLOBAL &&
                    packet.getDirection() == Packet::DIRECTION_RESPONSE &&
                    packet.getMessageNumber() == 0) {
                    
                    FindDevicesReply reply(packet);
                    LOG_INFO("Found device id=%d host=%s port=%u desc = %s", 
                        packet.getDeviceId(), inet_ntoa(deviceAddr.sin_addr), 
                        reply.getPort(), reply.getDescription());
                    Device* device = makeDevice(reply, deviceAddr);
                    std::auto_ptr<Device> devicePtr(device);
                    deviceMap->insert(packet.getDeviceId(), devicePtr);
                }
            }
        }

        netutil::setBlocking(serverSock_);

        DeviceMapPtr deviceMapPtr(deviceMap);
        return deviceMapPtr;
    }

    void UdpConnMgr::doSend(const Device& device, const Packet& packet) {
        const UdpDeviceInfo* devInfo = static_cast<const UdpDeviceInfo*>(device.getDeviceInfo());
        int sendSize = packet.getTotalSize();
        const sockaddr_in& devAddr = devInfo->getAddr();
        LOG_TRACE("sending packet ip = %s port = %d", inet_ntoa(devAddr.sin_addr), ntohs(devAddr.sin_port));
        if (sendto(deviceSock_, packet.getBuffer(), sendSize, 0, 
            reinterpret_cast<const sockaddr*> (&devAddr), sizeof(devAddr)) != sendSize) {
            THROW(ErrnoException, "Failed to send  packet");
        }
        LOG_TRACE("sent packet", 0);
    }

    UdpConnMgr::~UdpConnMgr() {}

    void UdpConnMgr::createServerSock() {
        serverSock_ = netutil::createUdpSock();
        netutil::bindSock(serverSock_, port_);
    }

    Device* UdpConnMgr::makeDevice(const FindDevicesReply& msg, 
        const sockaddr_in& devAddr) {
        sockaddr_in properAddr(devAddr);
        properAddr.sin_port = htons(msg.getPort());
        UdpDeviceInfo* devInfo = new UdpDeviceInfo(properAddr);
        return new Device(msg.getDeviceId(), msg.getDescription(), this, devInfo);
    }

    IncomingPacket* UdpConnMgr::doGetNextPacket() {
        sockaddr_in deviceAddr;
        socklen_t addrLen = sizeof(deviceAddr);
        int received;
        
        do {
            if ((received = recvfrom(serverSock_, buffer_, BUFFER_SIZE, 0, 
                            reinterpret_cast<sockaddr*> (&deviceAddr), &addrLen)) < 0) {
                if (errno == EAGAIN) {
                    THROW(ErrnoException, "still nonblock");
                }
                THROW(ErrnoException, "Failed to receive packet");
            } 
            LOG_TRACE("Received some packet", 0);
        } while (received < Packet::HEADER_SIZE);
        return new IncomingPacket(buffer_, received - Packet::HEADER_SIZE);
    }
}
